package support

import (
	"fmt"
	"gitee.com/gcom/gbox/errors"
	"github.com/jinzhu/gorm"
	"go.uber.org/zap"
	"reflect"
	"strings"
	"time"
)

type SimpleEntity struct {
	Id        int64     `gorm:"primary_key"`
	CreatedAt time.Time // 记录创建时间
	UpdatedAt time.Time // 最后更新时间
}

type CrudEntity struct {
	Id           int64     `gorm:"primary_key"`
	CreatedAt    time.Time // 记录创建时间
	CreatedBy    string    // 创建人
	CreateUserId int64     // 创建人ID
	UpdatedAt    time.Time // 最后更新时间
	UpdatedBy    string    // 更新人
	UpdateUserId int64     // 更新人ID
	Deleted      bool      // 是否标记删除
	DeletedAt    time.Time // 标记删除时间
	DeleteUserId int64     // 删除人ID
	DeletedBy    string    // 删除人
}

func BatchInsertOrUpdateOf(db *gorm.DB, values interface{}, canUpdate func(f *gorm.Field) bool) (int64, error) {
	return doBatchInsertOrUpdate(db, values, true, canUpdate)
}

func BatchInsertOrUpdate(db *gorm.DB, values interface{}, updateOnDup bool) (int64, error) {
	return doBatchInsertOrUpdate(db, values, updateOnDup, func(f *gorm.Field) bool {
		return f.Name != "Id" && (len(f.Name) <= 6 || f.Name[:6] != "Create")
	})
}

// BatchInsertOrUpdate 批量插入
// values 插入的实体数组或切片
// updateOnDup 插入重复时是否更新
func doBatchInsertOrUpdate(db *gorm.DB, values interface{}, updateOnDup bool, canUpdate func(f *gorm.Field) bool) (int64, error) {
	db = db.New().LogMode(false)
	// 参数转换
	var entities []interface{}
	val := reflect.ValueOf(values)
	if val.Kind() != reflect.Slice && val.Kind() != reflect.Array {
		return 0, errors.New("entities参数必须为数组或切片")
	}
	entities = make([]interface{}, val.Len())
	for i := 0; i < val.Len(); i++ {
		entities[i] = val.Index(i).Interface()
	}

	if len(entities) == 0 {
		return 0, nil
	}

	if len(entities) > 200 {
		c1, err := BatchInsertOrUpdate(db, entities[:200], updateOnDup)
		if err != nil {
			return c1, err
		}
		c2, err := BatchInsertOrUpdate(db, entities[200:], updateOnDup)
		return c1 + c2, err
	}

	mainObj := entities[0]
	mainScope := db.NewScope(mainObj)
	mainFields := mainScope.Fields()
	quoted := make([]string, 0, len(mainFields))
	for i := range mainFields {
		// If primary key has blank value (0 for int, "" for string, nil for interface ...), skip it.
		// If field is ignore field, skip it.
		if (mainFields[i].IsPrimaryKey && mainFields[i].IsBlank) || (mainFields[i].IsIgnored) {
			continue
		}
		quoted = append(quoted, mainScope.Quote(mainFields[i].DBName))
	}

	placeholdersArr := make([]string, 0, len(entities))
	for _, obj := range entities {
		scope := db.NewScope(obj)
		fields := scope.Fields()

		placeholders := make([]string, 0, len(fields))
		for i := range fields {
			if (fields[i].IsPrimaryKey && fields[i].IsBlank) || (fields[i].IsIgnored) {
				continue
			}
			var vars interface{}
			if (fields[i].Name == "CreatedAt" || fields[i].Name == "UpdatedAt") && fields[i].IsBlank {
				vars = gorm.NowFunc()
			} else {
				vars = fields[i].Field.Interface()
			}
			placeholders = append(placeholders, mainScope.AddToVars(vars))
		}

		placeholdersStr := "(" + strings.Join(placeholders, ", ") + ")"
		placeholdersArr = append(placeholdersArr, placeholdersStr)
	}

	updates := ""
	if updateOnDup {
		fields := make([]*gorm.Field, 0, len(mainFields))
		for _, f := range mainFields {
			if canUpdate(f) {
				fields = append(fields, f)
			}
		}
		updates = buildConflictUpdates(mainScope, fields)
	}
	mainScope.Raw(fmt.Sprintf("INSERT INTO %s (%s) VALUES %s%s;",
		mainScope.QuotedTableName(),
		strings.Join(quoted, ", "),
		strings.Join(placeholdersArr, ", "),
		updates,
	))

	var ts = time.Now()
	var err = mainScope.Exec().DB().Error
	var duration = time.Now().Sub(ts)
	var rows = mainScope.DB().RowsAffected

	// Log
	if ctx, ok := db.Get(TracerCtxKey); ok {
		ctx.(TracerCtx).Logger.Desugar().WithOptions(zap.AddCallerSkip(2)).Info("gorm.sql", zap.String("SQL",
			fmt.Sprintf("INSERT INTO %s (%s) VALUES () %s... parameter size %d",
				mainScope.QuotedTableName(), strings.Join(quoted, ", "), updates, len(entities))),
			zap.Duration("duration", duration),
			zap.Int64("affected_rows", rows))
	}
	return rows, err
}

/// 拼接插入冲突时的更新语句
func buildConflictUpdates(scope *gorm.Scope, fields []*gorm.Field) string {
	pre := " ON DUPLICATE KEY UPDATE "
	switch len(fields) {
	case 0:
		return ""
	case 1:
		return pre + fields[0].DBName
	}

	vals := make([]string, 0, len(fields))
	for _, s := range fields {
		col := scope.Quote(s.DBName)
		vals = append(vals, col+"=VALUES("+col+")")
	}
	n := len(pre) + len(vals) - 1
	for i := 0; i < len(vals); i++ {
		n += len(vals[i])
	}

	var b strings.Builder
	sep := ","
	b.Grow(n)
	b.WriteString(pre)
	b.WriteString(vals[0])
	for _, s := range vals[1:] {
		b.WriteString(sep)
		b.WriteString(s)
	}
	return b.String()
}
